// Copyright (C) 2023 即时通讯网(52im.net) & Jack Jiang.
// The RainbowChat Project. All rights reserved.
// 
// 【本产品为著作权产品，合法授权后请放心使用，禁止外传！】
// 【本次授权给：<北京小羊驼科技有限公司>，授权编号：<NT231212144350>，代码指纹：<A.702363430.550>，技术对接人微信：<ID: wxid_wbyootvkdcgj22>】
// 【授权寄送：<收件：苏先生、地址：北京市大兴区北京密码西区6号楼1单元301、电话：18613885610、邮箱：bd@huiyuanxiang-inc.com>】
// 
// 【本系列产品在国家版权局的著作权登记信息如下】：
// 1）国家版权局登记名(简称)和权证号：RainbowChat    （证书号：软著登字第1220494号、登记号：2016SR041877）
// 2）国家版权局登记名(简称)和权证号：RainbowChat-Web（证书号：软著登字第3743440号、登记号：2019SR0322683）
// 3）国家版权局登记名(简称)和权证号：RainbowAV      （证书号：软著登字第2262004号、登记号：2017SR676720）
// 4）国家版权局登记名(简称)和权证号：MobileIMSDK-Web（证书号：软著登字第2262073号、登记号：2017SR676789）
// 5）国家版权局登记名(简称)和权证号：MobileIMSDK    （证书号：软著登字第1220581号、登记号：2016SR041964）
// * 著作权所有人：江顺/苏州网际时代信息科技有限公司
// 
// 【违法或违规使用投诉和举报方式】：
// 联系邮件：jack.jiang@52im.net
// 联系微信：hellojackjiang
// 联系QQ号：413980957
// 授权说明：http://www.52im.net/thread-1115-1-1.html
// 官方社区：http://www.52im.net
package com.x52im.rainbowchat;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.Vector;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.alibaba.fastjson.JSON;
import com.eva.epc.common.util.CommonUtils;
import com.eva.epc.common.util.RestHashMap;
import com.eva.framework.HttpController;
import com.eva.framework.Processor;
import com.eva.framework.Processor.User;
import com.eva.framework.dbpool.DBDepend;
import com.eva.framework.dto.DataFromClient;
import com.eva.framework.dto.DataFromServer;
import com.eva.framework.dto.LoginInfo2;
import com.eva.framework.dto.LogoutInfo;
import com.eva.framework.utils.EndsExceptionFactory;
import com.eva.framework.utils.LoggerFactory;
import com.google.gson.Gson;
import com.x52im.rainbowchat.cache.UsersInfoCacheProvider;
import com.x52im.rainbowchat.common.dto.AutoUpdateInfoFromServer;
import com.x52im.rainbowchat.common.dto.cnst.EncodeConf;
import com.x52im.rainbowchat.common.dto.cnst.MyProcessorConst;
import com.x52im.rainbowchat.common.dto.cnst.OperateLog;
import com.x52im.rainbowchat.http.logic.FileInfoProcessor;
import com.x52im.rainbowchat.http.logic.GiftProcessor2;
import com.x52im.rainbowchat.http.logic.GroupChatProcessor;
import com.x52im.rainbowchat.http.logic.IMAdminProcessor;
import com.x52im.rainbowchat.http.logic.LogicProcessor2;
import com.x52im.rainbowchat.http.logic.LogicUtils;
import com.x52im.rainbowchat.http.logic.dto.RosterElementEntity;
import com.x52im.rainbowchat.im.util.ChatServerUtils;
import com.x52im.rainbowchat.token.TokenManager;

/**
 * RainbowChat的服务端HTTP REST接口服务的MVC核心控制器实现类（用于APP等通过HTTP POST方式）.
 * <p>
 * <b>说明：</b>本子类只需实现用户级的业务逻辑.
 * <p>
 * <b>调用：</b>HTTP的POST方式。<br>
 * <b>用途：</b>本类主要用于实现APP端的http rest接口调用，服务端返回格式为JSON。
 * 
 * @author Jack Jiang(http://www.52im.net/space-uid-1.html)
 * @since 3.0
 */
public class MyControllerJSON extends HttpController
{
	private static final long serialVersionUID = 1L;

	/** 用于执行客户端登陆请求时的客户端程序版本检查的辅助对象 */
	private ClientAPKVersionChecker clientAPKVersionChecker = null;
	
	private GiftProcessor2 giftProcessor = new GiftProcessor2();
	private LogicProcessor2 logicProcessor = new LogicProcessor2();
	private IMAdminProcessor adminProcessor = new IMAdminProcessor();
	private FileInfoProcessor fileInfoProcessor = new FileInfoProcessor();
	private GroupChatProcessor groupChatProcessor = new GroupChatProcessor();
	
	/**
	 * <p>
	 * MVC框架核心调度处理方法.
	 * <p>
	 * RainbowChat的所有http rest数据接口请求，将由本方法捕获，并进行业务调度和处理。

	 * @param dfc 封装了客户端发过来的相关数据（本方法中是必须的参数）
	 * @param req 客户端请求实例引用（把它传过来完全是未了考虑以后可能有其它作用，本方法中它不是必须的）
	 * @param res 客户端响应实例引用（把它传过来完全是未了考虑以后可能有其它作用，本方法中它不是必须的）
	 * @return 处理完返回的对象（要给客户端的）
	 * @throws Exception 本方法调用中出现任何异常时都会抛出，方法将中断执行
	 */
	@Override protected Object dispatch(DataFromClient dfc, HttpServletRequest req, HttpServletResponse res) throws Exception
	{
		Object retObj = null;
		int processor_id = dfc.getProcessorId();
		// http接口编号
		String httpInterfaceNum = DataFromClient.constructHttpInterfaceNum(processor_id, dfc.getJobDispatchId(), dfc.getActionId());
		
		LoggerFactory.getLog().debug("〖 [HTTP][客户端类型:"+printOS(dfc.getDevice())+"] 捕获REST【接口" +httpInterfaceNum+"】的请求，安全token="+dfc.getToken()+" 〗");
		
		// Rest框架需要首先对客户端发送过来的数据进行类型检查，防止Android、标准Java这种java系
		// 的平台发过来的是除String之外的平台发过来的是除String之外的Java序列化对象而非JSON文本
		if((dfc.getNewData() != null && !(dfc.getNewData() instanceof String))
			|| (dfc.getOldData() != null && !(dfc.getOldData() instanceof String)))
		{
			String ds = "[HTTP]Rest框架要求客户端发过来的数据必须是JSON文本！而现在拿到的是：" +
					"newData="+(dfc.getNewData()!= null?dfc.getNewData().getClass().getName():"")
					+", oldData="+(dfc.getOldData()!= null?dfc.getOldData().getClass().getName():"");
			LoggerFactory.getLog().error(ds);
			throw new RuntimeException(ds);
		}
		
		Processor processor = null;
		
		// 【接口1009】登陆认证和版本检查综合接口（Android专用）
		if(processor_id == MyProcessorConst.PROCESSOR_LOGIN$CHECKUPDATE_4A)
		{
			LoginInfo2 ai = JSON.parseObject((String)dfc.getNewData(), LoginInfo2.class);
			String uidOrmail = ai.getLoginName();
			String clientCurrentVersion = ai.getClientVersion();
			String oprContent = ai.getDeviceInfo();
			LoggerFactory.getLog().debug("[HTTP]<版本检查START> ..................用户："+uidOrmail);
			
			//** 开始进行版本检查
			AutoUpdateInfoFromServer aui = getClientAPKVersionChecker().checkClientAPKVersion(clientCurrentVersion);
			//客户端需要更新
			if(aui.isNeedUpdate() && !("-1".equals(ai.getClientVersion())))
			{
				OperateLog oprLog = new OperateLog(OperateLog.OPERATE_LOG_TYPE_NEED_UPDATE, uidOrmail, oprContent);
				addOperateLog(oprLog);
				LoggerFactory.getLog().debug("[HTTP]<版本检查END> "+uidOrmail+"需首先更新程序版本("
						+"手机当前版本："+clientCurrentVersion+"，需要更新到版本："+aui.getLatestClientAPKVercionCode()
						+")，更新信息将立即返回给客户端，RETURN.");
//				return aui;
			}
				
			//** 登陆验证
			LoggerFactory.getLog().debug("[HTTP]<登陆时：登陆验证START> "+uidOrmail+"，接下来将进入真正的登陆验证流程.");
			// 登陆认证：返回null则表示认证实现，否则返回的是登陆用户的个人信息
			RosterElementEntity authedUser = authrizationLogin(ai, req.getRemoteHost(), true);
			if(authedUser == null)
			{
				LoggerFactory.getLog().debug("[HTTP]<登陆时：登陆验证END> "+uidOrmail+"的账号无效或登陆密码不正确(在其它手机修改过？).");
				OperateLog oprLog = new OperateLog(OperateLog.OPERATE_LOG_TYPE_LOGIN_FALIURE, uidOrmail, oprContent);
				addOperateLog(oprLog);
			}
			else
			{
				OperateLog oprLog = new OperateLog(OperateLog.OPERATE_LOG_TYPE_LOGIN_SUCCESS, uidOrmail, oprContent);
				addOperateLog(oprLog);
			}

			//** 返回值
			RestHashMap retMap = RestHashMap.n().p("update_info", JSON.toJSONString(aui));
			if(authedUser != null) {
				retMap.p("authed_info", JSON.toJSONString(authedUser));
			}
			
			return JSON.toJSONString(retMap);
		}
		// 【接口1010】客户端版本更新检查接口（Android专用）
		else if(processor_id == MyProcessorConst.PROCESSOR_CHECK_UPDATE_4A)
		{
			AutoUpdateInfoFromServer aui = new AutoUpdateInfoFromServer();
			if(dfc.getNewData() != null)
			{
				String clientCurrentVersion = (String)dfc.getNewData();
				//开始进行版本检查
				aui = getClientAPKVersionChecker().checkClientAPKVersion(clientCurrentVersion);
			}
			
			return JSON.toJSONString(aui);
		}
		// 【接口1013】客户端登陆接口（Android\ios均可使用）
		else if(processor_id == MyProcessorConst.PROCESSOR_LOGIN_4ALL)
		{
			LoginInfo2 ai = JSON.parseObject((String)dfc.getNewData(), LoginInfo2.class);
			
			String uidOrmail = ai.getLoginName();
			String oprContent = ai.getDeviceInfo();

			//** 登陆验证
			LoggerFactory.getLog().debug("[HTTP]<登陆时：通用登陆验证START> "+uidOrmail+"，接下来将进入真正的登陆验证流程.");
			// 登陆认证：返回null则表示认证实现，否则返回的是登陆用户的个人信息
			RosterElementEntity authedUser = authrizationLogin(ai, req.getRemoteHost(), false);
			if(authedUser == null)
			{
				LoggerFactory.getLog().debug("[HTTP]<登陆时：通用登陆验证END> "+uidOrmail+"的账号无效或登陆密码不正确(在其它手机修改过？).");
				OperateLog oprLog = new OperateLog(OperateLog.OPERATE_LOG_TYPE_LOGIN_FALIURE, uidOrmail, oprContent);
				addOperateLog(oprLog);
			}
			else
			{
				// 记录操作日志
				OperateLog oprLog = new OperateLog(OperateLog.OPERATE_LOG_TYPE_LOGIN_SUCCESS, uidOrmail, oprContent);
				addOperateLog(oprLog);
			}

			//** 返回值
			return JSON.toJSONString(authedUser);
		}
		// 【接口-2】客户端注销登陆接口（Android\ios\web均可使用）
		else if(processor_id == MyProcessorConst.PROCESSSOR_LOGOUT)
		{
			LogoutInfo ao = JSON.parseObject((String)dfc.getNewData(), LogoutInfo.class);
			authrizationLogout(ao, req.getRemoteHost());
			return null;
		}
		// 其它业务接口
		else
		{
			// 出于安全考虑，此token用于检查每一个http rest请求的合法性（token在用户登陆时由服务端生成并返回给客户端）
			TokenManager.verifyHttpToken(httpInterfaceNum, dfc.getToken());

			// token校验通过后就进入正常的接口服务
			switch(processor_id)
			{
				// 礼品相关的接口处理器
				case MyProcessorConst.PROCESSOR_GIFT:
					processor = giftProcessor;
					break;
				// IM的基本业务逻辑接口处理器
				case MyProcessorConst.PROCESSOR_LOGIC:
					processor = logicProcessor;
					break;
				// 专用于后台管理端的一些特权接口，以后代码可考虑独立出去，保证安全性
				case MyProcessorConst.PROCESSOR_ADMIN:
					processor = adminProcessor;
					break;
				// 大文件信息查询接口的处理器
				case MyProcessorConst.PROCESSOR_FILE:
					processor = fileInfoProcessor;
					break;
				// 群组聊天相关管理接口的处理器
				case MyProcessorConst.PROCESSOR_GROUP_CHAT:
					processor = groupChatProcessor;
					break;
				// 找不到处理器
				default:
					throw EndsExceptionFactory.INVALID_PROCESSOR_ID(processor_id);
			}

			String basePath = req.getScheme()+"://"+req.getServerName()+":"+req.getServerPort()+req.getContextPath() + "/";
//			System.out.println("[HTTP]basePath = " +  basePath);
			// 借用一个User对象，仅仅是用来存放Ip地址而已
			User u = new User();
			u.setIp(req.getRemoteHost());
			if (processor != null)
			{
				if(processor instanceof LogicProcessor2) {
					retObj = processor.process(dfc.getJobDispatchId()
							, dfc.getActionId(), dfc.getNewData()
							, new Object[]{basePath, dfc.getOldData()}
							, u);
				} else {
					retObj = processor.process(dfc.getJobDispatchId()
							, dfc.getActionId(), dfc.getNewData()
							, dfc.getOldData()
							, u);
				}
				
				// 返回值类型检查
				if(retObj != null && !(retObj instanceof String)) {
					String ds = "[HTTP]Rest框架中processor处理完成的返回值必须是JSON文本哦！(retObj="+retObj.getClass().getName()+")";
					LoggerFactory.getLog().error(ds);
					throw new RuntimeException(ds);
				}
			}
			
			return retObj;
		}
	}
	
	/**
	 * 获取Android的版本检查对象。
	 * 
	 * @return
	 */
	private ClientAPKVersionChecker getClientAPKVersionChecker()
	{
		if(clientAPKVersionChecker == null)
			clientAPKVersionChecker = new ClientAPKVersionChecker(this.getServletContext().getRealPath("/"));//项目的发布磁盘目录就是apk文件放置的目录（已含末尾“/“） //ClientAPKVersionChecker.getAPKPath());
		return clientAPKVersionChecker;
	}
	
	/**
	 *  记录用户操作日志。
	 */
	public static void addOperateLog(OperateLog log) throws Exception
	{
		Vector sqls = new Vector();
		Vector values = new Vector();
		// 先把好友从我的好友列表中删除
		sqls.add("INSERT INTO missu_oprlog(user_uid,opr_type,opr_desc,opr_content,opr_bz,opr_time) " +
				"VALUES (?,?,?,?,?,"+DBDepend.getDefaultDatetimeFunc()+")");
		values.add(new Object[]{log.getUser_uid(), log.getOpr_type(),log.getOpr_desc(),log.getOpr_content(),log.getOpr_bz()});
		LogicProcessor2.db.update(sqls, values, true);
	}
	
	/**
	 * 验证登陆用户的人信息.
	 * 
	 * @param ai 登陆信息封装对象
	 * @return 登陆成功则返回此用户的完整个人信息，登陆不成功则返回null
	 * @throws Exception
	 */
	public static RosterElementEntity authrizationLogin(LoginInfo2 ai, String ip, boolean isAndroid) throws Exception
	{
		// 登陆名可以是多种形式
		String uidOrMail = ai.getLoginName();
		// 登陆密码
		String psw = ai.getLoginPsw();

		// 首先校验APPKey，
		if(!verifyAPPKey(ai)) {
			// 如果校验不通过，直接近回null，将中止余下的登录认证逻辑并同时拒绝用户登录
			return null;
		}

		boolean isMail = CommonUtils.isEmail(uidOrMail);
		if(!isMail && !checkUidValid(uidOrMail)){
			LoggerFactory.getLog().warn("[HTTP]<登陆时：验证uid合法性> 不合法的uid="+uidOrMail +"[客户端类型："+ai.getOsType()+"]，登录验证立即停止！");
			return null;
		}

		String baseSql = LogicUtils.getUSER_INFI_BASE_SQL(null) +  " where user_status = 1" +
//				" 	and (ltrim(str(user_uid))='"+uidOrMail+"' or user_mail='"+uidOrMail+"')" +
				"	and "+(isMail?"user_mail='"+uidSQLSave(uidOrMail)+"'":"user_uid="+uidSQLSave(uidOrMail))+
				"	and user_psw='"+CommonUtils.encyptPassword(psw, "")+"'";
		RosterElementEntity userAuthedInfo = LogicUtils.constructUserInfo(LogicProcessor2.db.querySingleRowItems(baseSql));
		if(userAuthedInfo != null)
		{
			//************************************** 【1】从iOS设备上登陆的特殊处理 ****************************************
			// ios设备的token，用于实现对ios设备的离线消息推送（详见苹果的APNs服务）
			String iosDeviceToken = null;
			// 如果是来自苹果手机登陆的特殊处理
			if(ai.isIOS())
			{
				// 取出提交上来的ios device token字段（此字段目前只供ios设备使用）
				iosDeviceToken = ai.getDeviceID();
				LoggerFactory.getLog().debug("[HTTP]<iOS登陆特殊处理> 当前账号"+uidOrMail+"是从iOS设备登陆的（deviceToken="+iosDeviceToken+"） 》》》");
				if(!CommonUtils.isStringEmpty(iosDeviceToken, true))
				{
					// 以下代码逻辑用于查出此前在此ios设备上登陆过的用户，并设置这些账号的ios_device_token字段为空
					// 
					//*【补充说明】：为什么要设置在此ios设备上登陆过的用户的ios_device_token字段为空？
					//* - 根据ios的device token生成规则，原则上同一个APP在同一个手机上的token是相同的(某些情况下除外)，
					//* - 理想情况下不同的用户账号在同一台手机上登陆过，他们提交到服务端的device token是一样的，
					//* - 而苹果的APNs离线消息推送只能区分device token是哪个手机，但不能区分这个token是哪个手机上
					//* - 的具体的那个用户，所以这么做是为了防止出现：登陆过别的账号后之前的用户消息还会被推过来、
					//* - 以及不同的账号如果恰好在同一个群里的话，就有可能同时收到多条群推送而给用户”重复推送“的错觉，）
					Vector<Vector> lastUsersUseThisIOSDeviceToken = 
						LogicProcessor2.db.queryData("select user_uid from missu_users " 
								+"where ios_device_token='"+iosDeviceToken
								// 排除本次登陆的”我“自已
								+"' and user_uid<>"+userAuthedInfo.getUser_uid());
					
					if(lastUsersUseThisIOSDeviceToken.size()>0)
					{
						LoggerFactory.getLog().debug("[HTTP]<iOS登陆特殊处理>已查到共"+lastUsersUseThisIOSDeviceToken.size()+"个账号"
								+Arrays.toString(lastUsersUseThisIOSDeviceToken.toArray())
								+" 登陆该ios设备（deviceToken="+iosDeviceToken+"），马上设置这些账号的ios_device_token=null .......");
						
						// 置null
						LogicProcessor2.db.update("update missu_users set ios_device_token=null where ios_device_token=?"
							, new Object[]{iosDeviceToken}, false);
						
						for(Vector r : lastUsersUseThisIOSDeviceToken)
						{
							String lastUserUseThisIOSDeviceToken = (String)r.get(0);
							//** 【高速缓存】即时更新用户缓存数据（因为用户信息中的ios设备的消息推送token字段已经更新为null了）
							UsersInfoCacheProvider.getInstance().reloadOneFromDB(lastUserUseThisIOSDeviceToken);
						}
					}
					else
					{
						LoggerFactory.getLog().debug("[HTTP]<iOS登陆特殊处理> 没有查到在此设备上登陆过的其它账号（deviceToken="+iosDeviceToken+"），继续进行其它逻辑.");
					}
				}
			}

			//************************************** 【2】生成 token **********************************************
			String token = "N/A";
			// 已开启token校验
			if(BaseConf.getInstance().getTOKEN_FOR_HTTP_VERIFY_ENABLED()) {
				token = TokenManager.getInstance().generate(userAuthedInfo.getUser_uid());
				LoggerFactory.getLog().debug("[HTTP]<token校验> 已启用http token校验机制【√】，本次生成的token："+token);
			} else {
				LoggerFactory.getLog().debug("[HTTP]<token校验> 当前未启用http token校验机制【×】，无需生成token。");
			}
			userAuthedInfo.setToken(token);
			
			//************************************** 【3】更新登陆信息 **********************************************
			// 更新最近登陆时间、最近登陆ip、用户的ios设备id（如果是ios设备登陆的话）
			LogicProcessor2.db.update("update missu_users set latest_login_time=" +DBDepend.getDefaultDatetimeFunc()+
					",latest_login_time2=?, latest_login_ip=?, ios_device_token=?, http_token=? where user_uid=?"
					, new Object[]{System.currentTimeMillis(), ip, iosDeviceToken, token, userAuthedInfo.getUser_uid()}, false);

			//************************************** 【4】保留字段的使用 **********************************************
			// 目前android登陆时才需要进行手机端版本更新检查（ios的app store自动完成、web里不需要版本更新）
			if(isAndroid)
			{
				// ## 【开启即生效】字段_ex3用于开启客户端的强制更新功能，开启的逻辑是当客户端的当前版本未更新到此版本时，无条件阻止客户登陆（客户端自版本42起已启用））
				userAuthedInfo.setMustVersion4A("410");// 表示要强制更新的版本号(小于此版本号的都需要强制更新)
//				userAuthedInfo.setMustVersionDesc4A("This version includes important updates, you must complete update before login!");// 将用于客户端在ui上的提示信息
				userAuthedInfo.setMustVersionDesc4A("您的版本已过时，请升级客户端后再试，感谢理解！");// 将用于客户端在ui上的提示信息
			}
			
			//************************************** 【5】更新当前登陆用户缓存 **********************************************
			//** 【高速缓存】即时更新用户缓存数据（因为用户信息中的ios设备的消息推送token字段可能已经更新了）
			UsersInfoCacheProvider.getInstance().reloadOneFromDB(userAuthedInfo.getUser_uid());
		}
		return userAuthedInfo;
	}
	
	/**
	 * 注销登陆.
	 * <p>
	 * 注销登陆可在客户端：退出APP、切换APP时调用。除了iOS设备外，暂时其它客户端不强制非得进行注销登陆。
	 * 
	 * @param ao 注销登陆信息封装对象
	 * @throws Exception
	 * @since 4.3
	 */
	public static void authrizationLogout(LogoutInfo ao, String ip) throws Exception
	{
		LoggerFactory.getLog().debug("[HTTP]<注销登陆> 由uid="+ao.getUid() +"[客户端类型："+ao.getOsType()+"] 的账号发出的注销登陆请求 ...");
		
		// 如果是ios设备的注销登陆：则设置它的设备token为空（这样就收不到ios的离线消息推送了）
		if(ao.isIOS())
		{
			if(!ao.isDontClearDeviceToken())
			{
				LogicProcessor2.db.update("update missu_users set ios_device_token=null where user_uid=?"
						, new Object[]{ao.getUid()}, false);
				
				//** 【高速缓存】即时更新用户缓存数据（注销的时候，该用户在db中的ios device token字段已被置空了）
				UsersInfoCacheProvider.getInstance().reloadOneFromDB(ao.getUid());
			}
		}
		// 其它客户端类型在注销登陆时需要做的事情
		else
		{
			// do nothing
		}
		
		// 加入用户操作日志
		OperateLog oprLog = new OperateLog(OperateLog.OPERATE_LOG_TYPE_LOGOUT_HTTP_LOGIN, ao.getUid(), ao.getDeviceInfo());
		addOperateLog(oprLog);
	}
	
	
	/**
	 * 将数据对象发回给PC客户端.
	 * @param res 客户端响应HttpServletResponse对象
	 * @param objFromServer 要发送的可系列化的数据对象
	 * @throws Exception 如果在操作OutputStream时出现异常
	 */
	// 重写父类方法的目的是将父类中发出的是java序列化对象的方式改为JSON文本方式（以便跨平台（非java平台））
	@Override
	protected void sendToClient(HttpServletResponse res,DataFromServer objFromServer) throws IOException
	{
		String toCient = new Gson().toJson(objFromServer);
		// 发出的是JSON文本描述的DataFromClient对象
		byte[] bs = toCient.getBytes(EncodeConf.ENCODE_TO_CLIENT);//
		OutputStream out = res.getOutputStream();
		out.write(bs);
		out.flush();
		out.close();
		
		LoggerFactory.getLog().info("【<== 处理完成-返回JSON结果】:"+toCient);
	}

	/**
	 * 读取PC客户端传过来的数据对象。
	 * @param req 客户端请求HttpServletRequest对象
	 * @return 返回读取出来的客户端数据对象
	 * @throws IOException 如果req.getInputStream()出现异常时抛出
	 * @throws ClassNotFoundException 如果is.readObject()出现异常时抛出
	 */
	// 重写父类方法的目的是将父类中接收的是java序列化对象的方式改为JSON文本方式（以便跨平台（非java平台））
	@Override
	protected DataFromClient recieveFromClient(HttpServletRequest req) throws IOException, ClassNotFoundException
	{
		InputStream is = req.getInputStream();
		int ch;
		ByteArrayOutputStream bos = new ByteArrayOutputStream();
		while ((ch = is.read()) != -1)
			bos.write(ch);
		is.close();
		byte[] bs = bos.toByteArray();
		bos.close();
		// 接收的数据是JSON文本描述的DataFromClient对象
		String res = SQLSave(new String(bs, EncodeConf.DECODE_FROM_CLIENT));
//		System.out.println(res);
		LoggerFactory.getLog().info("【==> 收到接口请求-数据原型】:"+res);
		DataFromClient dfc = new Gson().fromJson(res, DataFromClient.class);//JSON.parseObject(res, DataFromClient.class);
//		System.out.println("getActionId="+dfc.getActionId()
//				+", getJobDispatchId="+dfc.getJobDispatchId()
//				+", getProcessorId="+dfc.getProcessorId()
//				+", getNewData="+dfc.getNewData()
//				);
		return dfc;
	}
	
	
	// 打印客户端设备类型
	private static String printOS(int osType)
	{
		if(0 == osType)
			return "Android";
		else if(1 == osType)
			return "iOS";
		else if(2 == osType)
			return "Web";
		else
			return "未知";
	}
	
//	// 打印rest接口的编号，方便调试
//	public static String printRestNum(int processorId, int jobDispatchId, int actionId)
//	{
//		String ret = "";
//		if(processorId > 0)
//			ret += String.valueOf(processorId);
//		if(jobDispatchId > 0)
//			ret += "-"+String.valueOf(jobDispatchId);
//		if(actionId > 0)
//			ret += "-"+String.valueOf(actionId);
//
//		return ret;
//	}

	/**
	 * 严格检查uid内容的合法性，防止被攻击。
	 *
	 * @param originalUidStr 用户输入的登陆uid原始字符串
	 * @return true表示符合规范，否则不规范
	 */
	public static boolean checkUidValid(String originalUidStr){
		return originalUidStr != null && originalUidStr.length()<=11 && ChatServerUtils.isInteger(originalUidStr);
	}

	/**
	 * 强制对用户登录时输入的uid进行sql注入保护（本方法将在全局SQL防注入策略的基础上进一步防护）。
	 *
	 * @param originalStr 用户输入的登陆uid原始字符串
	 * @return 防sql注入处理后的结果
	 */
	public static String uidSQLSave(String originalStr){
		if(originalStr != null){
			// 主要防护“10000 or 1=1”这种类型的非法uid
			return originalStr.replaceAll(" ", "");
		}
		return originalStr;
	}
	
	/**
	 * 强制保护因程序员的后台SQL语句未使用预编译语句而发生SQL注入攻击的风险。
	 * 
	 * @param originalStr
	 * @return
	 */
	public static String SQLSave(String originalStr)
	{
		if(originalStr != null)
		{
			// 此替换逻辑来自网上，实测会将整条SQL全部转空，不是最佳逻辑，20190324日已弃用。
			// 比如：此逻辑会导致web端传过来的html转义字符（如“&gt;”）被错误地整块替换掉
			//       newData字段，从而无法成功进行http rest的请求处理！
//			return originalStr.replaceAll(".*([';]+|(--)+).*", " ");
			
			// 将SQL注入风险关键字符替换成空格，启用于20190324日
			return originalStr.replaceAll("'", " ").replaceAll(";", " ").replaceAll("--", " ");
		}
		
		return originalStr;
	}

	/**
	 * 校验客户端登录时提交过来的APPKey。
	 *
	 * @param ai 登录信息
	 * @return true表示校验通过，否则表示不通过
	 * @throws RuntimeException 如果校验不通过则抛出异常，否则表示校验通过
	 */
	public static boolean verifyAPPKey(LoginInfo2 ai) throws RuntimeException {
		if(BaseConf.getInstance().getAPPKEY_VERIFY_ENABLED()) {
			if(!BaseConf.getInstance().getAPPKEY_GRANTED().equals(ai.getAppKey())) {
				LoggerFactory.getLog().error("[HTTP]<登陆时：验证APPKey> 不合法的APPKey="+ai.getAppKey()+"，本次登录被拒【×】！(当前服务端APPKEY_GRANTED="+BaseConf.getInstance().getAPPKEY_GRANTED()+")");
//				throw new SecurityException("不合法的APPKey="+ai.getAppKey());
				return false;
			} else {
				LoggerFactory.getLog().info("[HTTP]<登陆时：验证APPKey> 客户端APPKey验证通过【√】，放行进入登录认证逻辑。。。");
			}
		} else {
			LoggerFactory.getLog().warn("[HTTP]<登陆时：验证APPKey> 当前服务端未开始APPKey校验哦【☠】。");
		}

		return true;
	}
	
//	public static void main(String[] args)
//	{
//		String s = "select * from users where username='';DROP Database (DB Name) --' and password=''";
//		System.out.println(">>>>>>>>>>>>>>>>>>>"+MyControllerJSON.SQLSave(s));
//	}
}
